{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "260811a0",
   "metadata": {},
   "source": [
    "<center>基于用户的协同过滤算法(user-based collaberative filtering algorithm)</center>\n",
    "\n",
    "早期的推荐系统(Recommender System)基本上是基于内容过滤（Content-based Filtering），完全依赖物品属性（如电影类型、关键字等），通过匹配用户历史偏好与物品特征进行推荐。这种推荐方式有很多局限性：\n",
    "- 要求内容高度结构化（如商品描述需精确标注）；\n",
    "- 无法捕捉复杂兴趣（如“悬疑喜剧”的混合偏好）；\n",
    "- 推荐结果同质化（仅推荐相似内容，缺乏惊喜）。  \n",
    "  \n",
    "1992年，Tapestry系统首次实现协同过滤原型，解决施乐公司邮件过载问题。其创新在于**记录用户主观评价**（如标记有用邮件），形成个性化过滤器。1994年，GroupLens系统正式提出“协同过滤”术语，并建立了数学模型，实现了群体智慧规模化应用。奠定了基于用户的协同过滤（User-CF）基础。\n",
    "\n",
    "## User-CF核心原理\n",
    "基于用户的协同过滤（User-based Collaborative Filter, User-CF）通过分析用户行为数据，找到与目标兴趣相似的群体，利用这些相似用户的偏好预测目标用户对未评价物品的偏好。\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "13ca6e2b",
   "metadata": {},
   "source": [
    "### 1. 用户相似度计算  \n",
    "用户相似度基于两个用户**共同评价过的物品**进行相似度计算。计算用户相似度有多重方法，常用的有皮尔逊相关系数和余弦相似度。这里我们采用余弦相似度，计算公式如下：\n",
    "$$cos(\\theta)=\\frac{A\\cdot B}{||A||||B||}$$\n",
    "- 分子：向量点积（$A\\cdot B=\\sum_{i=1}^kA_iB_i$）\n",
    "- 分母：向量的模长乘积（$||A||=\\sqrt{\\sum A_i^2}$,||B||计算同样）"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ead5f4d2",
   "metadata": {},
   "source": [
    "所有用户之间的相似度构成一个(n_users x n_users)的方阵，简称用户相似度矩阵，方便后面预测时使用。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d755eec1",
   "metadata": {},
   "source": [
    "### 2. 预测目标用户的评分\n",
    "通过上述计算的用户相似度，利用相似用户的评分预测目标用户对未评分物品的偏好。其核心公式如下：\n",
    "$$\\hat r_{ui}=\\overline r_u+\\frac{\\sum_{v\\in N(u)}w_{uv}\\cdot(r_{vi}-\\overline r_v)}{\\sum_{v\\in N(u)}|w_{uv}|}$$"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "44df12c9",
   "metadata": {},
   "source": [
    "该公式就是**目标用户u对物品i的预测评分**$\\hat r_{ui}$计算公式。其中\n",
    "- $\\overline r_u$：用户u已评分物品的平均评分。\n",
    "- $N(u)$：用户u的近邻用户集合（相似度最高的k个用户）。\n",
    "- $w_{uv}$：用户u与v的相似度。\n",
    "- $r_{vi}$：用户v对物品i的评分。\n",
    "- $\\overline r_v$：用户v对已评分物品的平均评分。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5655cc1c",
   "metadata": {},
   "source": [
    "## User-CF编程实现\n",
    "基于小容量数据编写，目的就是为了复现User-CF算法。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "786208f3",
   "metadata": {},
   "source": [
    "### 1. 构建用户-物品矩阵  \n",
    "矩阵的行表示用户，列表示物品，矩阵元素为用户对物品的评分（如点击、购买、点赞等行为量化后的值）。例如："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "7b14ff5a",
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np \n",
    "\n",
    "# 评分矩阵（用户-物品）\n",
    "ratings = np.array([\n",
    "    [5, 3, 0, 1],  # 用户 0\n",
    "    [4, 0, 0, 1],  # 用户 1\n",
    "    [1, 1, 0, 5],  # 用户 2\n",
    "    [1, 0, 0, 4],  # 用户 3\n",
    "    [0, 1, 5, 4]   # 用户 4\n",
    "])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "32d1de65",
   "metadata": {},
   "source": [
    "### 2. 构建用户相似度矩阵\n",
    "用户相似度矩阵是通过计算用户间的相似度得到。这里采用余弦相似度。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "39964d46",
   "metadata": {},
   "outputs": [],
   "source": [
    "def user_similarity(ratings):\n",
    "    \"\"\"计算用户相似度矩阵（基于共同评分项目的余弦相似度）\"\"\"\n",
    "    n_users = ratings.shape[0] # 用户个数\n",
    "    similarity = np.zeros((n_users,n_users)) # 初始化用户相似度矩阵为(n_users,n_users)全零矩阵\n",
    "\n",
    "    for i in range(n_users):\n",
    "        for j in range(i+1,n_users): # 只计算上三角，避免重复计算\n",
    "            # 找出共同评分的项目\n",
    "            common_mask = (ratings[i]>0) & (ratings[j]>0)\n",
    "            if np.any(common_mask):\n",
    "                # 提取公共评分向量\n",
    "                vec_i = ratings[i,common_mask]\n",
    "                vec_j = ratings[j,common_mask]\n",
    "\n",
    "                # 计算余弦相似度\n",
    "                dot_product = np.dot(vec_i,vec_j)\n",
    "                norm_i = np.linalg.norm(vec_i)\n",
    "                norm_j = np.linalg.norm(vec_j)\n",
    "\n",
    "                if norm_i>0 and norm_j>0:\n",
    "                    sim = dot_product/(norm_i*norm_j)\n",
    "                    similarity[i,j] = sim\n",
    "                    similarity[j,i] = sim # 对称矩阵\n",
    "                    \n",
    "\n",
    "    return similarity"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "56899aba",
   "metadata": {},
   "source": [
    "### 3. 预测评分\n",
    "根据相似用户（多个）对某个物品的评分，计算目标用户对该物品的评分预测。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "ebbd057b",
   "metadata": {},
   "outputs": [],
   "source": [
    "def predict_rating(ratings,sim_matrix,user_id,item_id,k=2):\n",
    "    \"\"\"预测指定用户对物品的评分\"\"\"\n",
    "    # 计算目标用户的平均评分(只计算评价过的物品的评分)\n",
    "    user_ratings = ratings[user_id]\n",
    "    user_rated_items = user_ratings>0\n",
    "\n",
    "    if np.any(user_rated_items):\n",
    "        user_mean = np.mean(user_ratings[user_rated_items]) \n",
    "    else:\n",
    "        user_mean = 0\n",
    "\n",
    "    # 获取所有其他用户的索引\n",
    "    n_users = ratings.shape[0]\n",
    "    other_users = np.array([i for i in range(n_users) if i !=user_id])\n",
    "    \n",
    "    # 仅考虑对目标物品有评分的用户\n",
    "    rated_mask = ratings[other_users,item_id]>0\n",
    "    rated_users = other_users[rated_mask]\n",
    "      \n",
    "    if len(rated_users)==0: # 没有用户对该物品评分\n",
    "        return user_mean   \n",
    "    \n",
    "    # 获取这些用户与目标用户的相似度\n",
    "    user_similarities = sim_matrix[user_id,rated_users]\n",
    "\n",
    "    # 按照相似度降序排序(找到相似度最高的k个近邻)\n",
    "    sorted_indices = np.argsort(user_similarities)[::-1]\n",
    "    top_k = min(k,len(sorted_indices))\n",
    "    neighbor_indices = rated_users[sorted_indices[:top_k]]\n",
    "    \n",
    "    # 获取邻居的评分和相似度\n",
    "    neighbor_ratings = ratings[neighbor_indices,item_id]\n",
    "    neighbor_sims = sim_matrix[user_id,neighbor_indices]\n",
    "    \n",
    "    # 计算每个近邻的平均评分\n",
    "    neighbor_means = []\n",
    "    for idx in neighbor_indices:\n",
    "        u_ratings = ratings[idx]\n",
    "        mask = u_ratings>0\n",
    "        if np.any(mask):\n",
    "            neighbor_means.append(np.mean(u_ratings[mask]))\n",
    "        else:\n",
    "            neighbor_means.append(0)\n",
    "    neighbor_means = np.array(neighbor_means)\n",
    "    \n",
    "    # 计算偏差调整后的评分\n",
    "    adjusted_ratings = neighbor_ratings-neighbor_means\n",
    "\n",
    "    # 计算加权平均偏差\n",
    "    numerator = np.dot(adjusted_ratings,neighbor_sims)    \n",
    "    denominator = np.sum(np.abs(neighbor_sims))\n",
    "    if denominator > 0:\n",
    "        # 计算预测评分并限制在0-5范围内\n",
    "        prediction = user_mean + numerator/denominator\n",
    "        return max(0,min(5,prediction))\n",
    "    else:\n",
    "        return user_mean"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "035aad2b",
   "metadata": {},
   "source": [
    "### 测试\n",
    "预测用户0对物品2的评分。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "f1be4fe2",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "用户相似度矩阵：\n",
      "[[0.     0.9989 0.4229 0.4281 0.5369]\n",
      " [0.9989 0.     0.4281 0.4706 1.    ]\n",
      " [0.4229 0.4281 0.     0.9989 0.9989]\n",
      " [0.4281 0.4706 0.9989 0.     1.    ]\n",
      " [0.5369 1.     0.9989 1.     0.    ]]\n",
      "用户0对物品2的预测评分:4.67\n"
     ]
    }
   ],
   "source": [
    "# 计算用户相似度矩阵\n",
    "sim_matrix = user_similarity(ratings)\n",
    "print(f\"用户相似度矩阵：\\n{np.round(sim_matrix,4)}\")\n",
    "\n",
    "# 预测用户0对物品2的评分\n",
    "user_id = 0\n",
    "item_id = 2\n",
    "prediction = predict_rating(ratings,sim_matrix,user_id,item_id,k=2)\n",
    "print(f\"用户{user_id}对物品{item_id}的预测评分:{prediction:.2f}\")"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "da (3.11.4)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.4"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
